package com.qire.manhua.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;

import com.qire.manhua.R;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Objects;

/**
 * 平铺布局,一个流式平铺布局，从左到右，从上至下，平铺不够空间自动换行。
 *  支持强制换行和根据权重自动适配宽度。
 */
public class TileLayout extends ViewGroup {

    /**
     * 循环利用回收站
     */
    private final RecycleBin recycleBin = new RecycleBin();

    /**
     * 数据元素项目数
     */
    private int mItemCount;

    /**
     * 数据适配器
     */
    private BaseAdapter mAdapter;

    /**
     * 适配数据观察者
     */
    private AdapterDataSetObserver mDataSetObserver;

    public TileLayout(Context context) {
        this(context, null);
    }

    public TileLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TileLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public TileLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        final int childCount = getChildCount();
        // 测量参数，用于记录测量宽高，布局换行等操作。
        final MeasureParam measureParam = new MeasureParam(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));

        // 遍历所有子View
        for(int index = 0; index < childCount; index++) {

            // 最后一个 View
            boolean isEnd = index == (childCount - 1);

            final View child = getChildAt(index);
            final LayoutParams childLP = (LayoutParams) child.getLayoutParams();

            // 基础测量：针对非隐藏View，保证非特殊 View 获得基本原生View的测量属性。
            if(child.getVisibility() != GONE) {
                // 通用测量处理
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                measureParam.combineMeasuredStates(child.getMeasuredState());

                // 记录当前显示的控件权重占比，如果权重为0或负数不记录占比
                // 当前并不知道一共有多少权重行，固累加计数，在下一个非权重行或结尾时在处理
                measureParam.addWeight(childLP.weight);
            }

            // 判断是否存在未处理的权重View
            if(measureParam.existWeight()) {
                // 判断是否达到处理权重View的条件：最后一个View、存在换行描述、权重连续中断
                if(isEnd || childLP.isLineBreak || childLP.notWeight()) {
                    int weightWidth = measureParam.measureWeightWidth();

                    // 判断当前View是否需要包含入本次权重行处理，强制换行和结束遍历时当前View属于权重View则需要包含
                    boolean needContain = (isEnd && childLP.isWeight()) || (childLP.isLineBreak && childLP.isWeight());

                    int end = needContain ? index + 1 : index;
                    int begin = measureParam.beginWeight(end);

                    for(int i = begin; i < end; i++) {
                        View weightChild = getChildAt(i);
                        LayoutParams weightLP = (LayoutParams) weightChild.getLayoutParams();
                        if(weightLP.isWeight()) {
                            measureLineChild(weightChild, weightWidth * weightLP.weight);
                            measureParam.updateLineMaxHeight(childRealHeight(weightChild.getMeasuredHeight(), weightLP));
                        }
                    }

                    measureParam.lineBreak();
                    measureParam.resetWeight();
                }
            }

            // 处理显示的非权重View
            if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                measureParam.measureMaxWidth(childWidth);

                if(childLP.notWeight()) {
                    // 处理普通平铺流程
                    int realWidth = childRealWidth(childWidth, childLP);
                    if(measureParam.checkLineBreak(realWidth)) {
                        measureParam.lineBreak();
                    }
                    measureParam.updateLastWidth(realWidth);
                    measureParam.updateLineMaxHeight(childRealHeight(childHeight, childLP));

                    if(childLP.isLineBreak) {
                        // 强制换行标记
                        measureParam.lineBreak();
                    }
                }
            }

            if(isEnd) {
                measureParam.lineBreak();
            }

        }

//        setMeasuredDimension(measureWidth, heightSize);
        setMeasuredDimension(measureParam.getMeasuredWidth(widthMeasureSpec), measureParam.getMeasuredHeight(heightMeasureSpec));
    }

    private void measureLineChild(View child, int childWidth) {
        LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();

        if (layoutParams == null) {
            layoutParams = generateDefaultLayoutParams();
            child.setLayoutParams(layoutParams);
        }

        int margin = layoutParams.leftMargin + layoutParams.rightMargin;
        if(margin > childWidth) {
            layoutParams.leftMargin = layoutParams.rightMargin = (childWidth / 2);
            margin = childWidth;
        }

        final int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth - margin, MeasureSpec.EXACTLY);
        // 目前经过第一次测量所有子控件都是包裹模式的，如果需要处理撑满这些，需要额外处理，暂不处理。
//        final int childHeightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
        // 不写死使用 MeasureSpec.EXACTLY 是因为如果为 wrap_content 状态的话，就会导致父容器布局度量失效，会使用最大值，
        // wrap_content 需要使用 AT_MOST 模式， MATCH_PARENT 和 固定值需要使用 EXACTLY 模式，所以这里从子控件中取模式传递
        final int childHeightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.getMode(layoutParams.height));

        child.measure(childWidthSpec, childHeightSpec);
        child.forceLayout();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutChildren(changed, l, t, r, b);
    }

    private void layoutChildren(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();

        final MeasureParam measureParam = new MeasureParam(getMeasuredWidth(), getMeasuredHeight());

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                final int realWidth = childRealWidth(width, layoutParams);
                final int realHeight = childRealHeight(height, layoutParams);

                int childTop, childLeft;

                if(measureParam.checkLineBreak(realWidth)) {
                    measureParam.lineBreak();
                }

                childTop = measureParam.maxHeight + layoutParams.topMargin;
                childLeft = measureParam.lastWidth + layoutParams.leftMargin;

                measureParam.updateLastWidth(realWidth);
                measureParam.updateLineMaxHeight(realHeight);

                if(layoutParams.isLineBreak) {
                    measureParam.lineBreak();
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

    private int childRealHeight(int childHeight, LayoutParams childLP) {
        return childHeight + childLP.topMargin + childLP.bottomMargin;
    }

    private int childRealWidth(int childWidth, LayoutParams childLP) {
        return childWidth + childLP.leftMargin + childLP.rightMargin;
    }

    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams();
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        if (Build.VERSION.SDK_INT >= 19 && p instanceof LinearLayout.LayoutParams) {
            return new LayoutParams((LinearLayout.LayoutParams)p);
        } else {
            return p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams)p) : new LayoutParams(p);
        }
    }

    public void setAdapter(BaseAdapter adapter) {
        // 如果存在就注销，重新接受新的。这段如果引起异常也可以移除
        if(mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        mAdapter = adapter;
        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();

        if (mDataSetObserver == null) {
            mDataSetObserver = new AdapterDataSetObserver();
        }
        if (mAdapter != null) {
            mAdapter.registerDataSetObserver(mDataSetObserver);
        }
        fillData();
        requestLayout();
    }

    /**
     * 填充数据
     */
    private void fillData() {
        recycleBin.fillData();
        invalidate();
    }

    private class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            mItemCount = mAdapter.getCount();
            fillData();
            requestLayout();
        }

        @Override
        public void onInvalidated() {
            mItemCount = 0;
            requestLayout();
        }
    }

    private final class RecycleBin {

        //WeakHashMap
        private final ArrayList<ViewWrapper> recycleViewList = new ArrayList<>();
        private final int typeTag = 10 << 24;

        private RecycleBin() {}

        public void reset() {
            int childCount = getChildCount();
            if(childCount == 0) {
                return;
            }
            recycleViewList.clear();
            for(int index = 0; index < childCount; index++) {
                View view = getChildAt(index);
                recycleViewList.add(new ViewWrapper(view, (int)view.getTag(typeTag)));
            }
            removeAllViewsInLayout();
        }

        public void fillData() {
            reset();
            for(int index = 0; index < mItemCount; index++) {
                int typeId = mAdapter.getItemViewType(index);

                int i = recycleViewList.indexOf(typeId);

                View view = (i != -1) ? recycleViewList.remove(i).view() : null;

                view = mAdapter.getView(index, view, TileLayout.this);
                addViewInLayout(view,  -1, view.getLayoutParams(), true);
                view.setTag(typeTag, typeId);
            }
            recycleViewList.clear();
        }

        private class ViewWrapper {
            private final WeakReference<View> viewReference;
            private final int type;

            ViewWrapper(View view, int type) {
                this.viewReference = new WeakReference<>(view);
                this.type = type;
            }

            public View view() {
                return viewReference.get();
            }

            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;
                ViewWrapper that = (ViewWrapper) o;
                return type == that.type || Objects.equals(viewReference, that.viewReference);
            }

            @Override
            public int hashCode() {
                return Objects.hash(viewReference, type);
            }
        }

    }

    /**
     * 测量参数。
     */
    private final class MeasureParam {

        /** 父布局控件大小 */
        final int measureWidth, measureHeight;

        /** 缓存度量当前的最大宽度和高度 */
        int maxWidth = 0, maxHeight = 0;
        /** 测量一行当前View时最后一次最大宽度和高度 */
        int lastWidth = 0, lineMaxHeight = 0;
        /** 权重View 个数及权重和*/
        int lineChildCount =0, lineChildWeight = 0;

        int childState = 0;

        public MeasureParam(int measureWidth, int measureHeight) {
            this.measureWidth = measureWidth;
            this.measureHeight = measureHeight;
        }

        /**
         * 根据父控件模式决定返回宽度
         * @param widthMeasureSpec 包含父控件宽度及model
         * @return 使用精准宽度，还是使用包裹宽度
         */
        public int getMeasuredWidth(int widthMeasureSpec) {
            return resolveSizeAndState(maxWidth, widthMeasureSpec, childState);
        }

        /**
         * 根据父控件模式决定返回高度
         * @param heightMeasureSpec 包含父控件高度及model
         * @return
         */
        public int getMeasuredHeight(int heightMeasureSpec) {
            return resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT);
        }

        public void measureMaxWidth(int childWidth) {
            maxWidth = (maxWidth >= measureWidth) ? measureWidth : maxWidth + childWidth;
        }

        /**
         * 更新一行测量中最后的宽度
         * @param realWidth
         */
        public void updateLastWidth(int realWidth) {
            this.lastWidth += realWidth;
        }

        /**
         * 更新一行测量中最大高度
         * @param realHeight
         */
        public void updateLineMaxHeight(int realHeight) {
            this.lineMaxHeight = Math.max(lineMaxHeight, realHeight);
        }

        /**
         * 检查子控件是否能被放在这一行
         * @param realWidth
         * @return
         */
        public boolean checkLineBreak(int realWidth) {
            return lastWidth + realWidth > measureWidth;
        }

        /**
         * 换行
         */
        public void lineBreak() {
            this.maxHeight += this.lineMaxHeight;
            this.lineMaxHeight = this.lastWidth = 0;
        }

        public void combineMeasuredStates(int newChildState) {
            this.childState = TileLayout.combineMeasuredStates(this.childState, newChildState);
        }

        public void resetWeight() {
            this.lineChildCount = 0;
            this.lineChildWeight = 0;
        }

        public void addWeight(int weight) {
            if(weight > 0) {
                this.lineChildCount++;
                this.lineChildWeight += weight;
            }
        }

        public boolean existWeight() {
            return lineChildWeight > 0;
        }

        public int measureWeightWidth() {
            // 如果权重行计数不为0 则处理权重行度量。
            int lineWidth = measureWidth - lastWidth;
            if(lineWidth <= 0) {
                lineBreak();
                lineWidth = measureWidth;
            }
            return lineWidth / lineChildWeight;
        }

        public int beginWeight(int indexCurrent) {
            return indexCurrent - lineChildCount;
        }

    }

    /**
     * 布局参数
     */
    public static class LayoutParams extends MarginLayoutParams {

        public static final int DEF_WEIGHT = 0;

        int weight = 0;

        public boolean isLineBreak = false;

        public LayoutParams() {
            this(MATCH_PARENT, WRAP_CONTENT);
        }

        public LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TileLayout);
            if(typedArray.hasValue(R.styleable.TileLayout_layout_weight)) {
                weight = typedArray.getInt(R.styleable.TileLayout_layout_weight, DEF_WEIGHT);
            }
            if(typedArray.hasValue(R.styleable.TileLayout_layout_lineBreak)){
                isLineBreak = typedArray.getBoolean(R.styleable.TileLayout_layout_lineBreak, false);
            }
            typedArray.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        public LayoutParams(LayoutParams source) {
            super(source);
            this.weight = source.weight;
            this.isLineBreak = source.isLineBreak;
        }

        public boolean isWeight() {
            return weight > LayoutParams.DEF_WEIGHT;
        }

        public boolean notWeight() {
            return !isWeight();
        }

    }

}
